use crate::util::path_to_string;
use kind::ErrorKind;
use proc_macro2::{Span, TokenStream};
use std::error::Error as StdError;
use std::fmt::{Display, Formatter};
use syn::{spanned::Spanned, Expr, Lit, LitStr, Path};

pub mod kind;

#[derive(Debug, Clone)]
pub struct Error {
    kind: ErrorKind,
    locations: Vec<String>,
    span: Option<Span>,
}

impl Error {
    pub fn new(kind: ErrorKind) -> Self {
        Error {
            kind,
            locations: Vec::new(),
            span: None,
        }
    }

    pub fn custom<T: Display>(msg: T) -> Self {
        Error::new(ErrorKind::Custom(msg.to_string()))
    }

    pub fn duplicate_field(name: &str) -> Self {
        Error::new(ErrorKind::DuplicateField(name.into()))
    }

    pub fn duplicate_field_path(path: &Path) -> Self {
        Error::duplicate_field(&path_to_string(path))
    }

    pub fn missing_field(name: &str) -> Self {
        Error::new(ErrorKind::MissingField(name.into()))
    }

    pub fn unknown_field(name: &str) -> Self {
        Error::new(ErrorKind::UnknownField(name.into()))
    }

    pub fn unknown_field_path(path: &Path) -> Self {
        Error::unknown_field(&path_to_string(path))
    }

    pub fn unsupported_shape(shape: &str) -> Self {
        Error::new(ErrorKind::UnsupportedShape {
            observed: shape.into(),
            expected: None,
        })
    }

    pub fn unsupported_shape_with_expected<T: Display>(shape: &str, expected: &T) -> Self {
        Error::new(ErrorKind::UnsupportedShape {
            observed: shape.into(),
            expected: Some(expected.to_string()),
        })
    }

    pub fn unsupported_format(format: &str) -> Self {
        Error::new(ErrorKind::UnexpectedFormat(format.into()))
    }

    pub fn unexpected_type(ty: &str) -> Self {
        Error::new(ErrorKind::UnexpectedType(ty.into()))
    }

    pub fn unexpected_expr_type(expr: &Expr) -> Self {
        Error::unexpected_type(match *expr {
            Expr::Array(_) => "array",
            Expr::Assign(_) => "assign",
            Expr::Async(_) => "async",
            Expr::Await(_) => "await",
            Expr::Binary(_) => "binary",
            Expr::Block(_) => "block",
            Expr::Break(_) => "break",
            Expr::Call(_) => "call",
            Expr::Cast(_) => "cast",
            Expr::Closure(_) => "closure",
            Expr::Const(_) => "const",
            Expr::Continue(_) => "continue",
            Expr::Field(_) => "field",
            Expr::ForLoop(_) => "for_loop",
            Expr::Group(_) => "group",
            Expr::If(_) => "if",
            Expr::Index(_) => "index",
            Expr::Infer(_) => "infer",
            Expr::Let(_) => "let",
            Expr::Lit(_) => "lit",
            Expr::Loop(_) => "loop",
            Expr::Macro(_) => "macro",
            Expr::Match(_) => "match",
            Expr::MethodCall(_) => "method_call",
            Expr::Paren(_) => "paren",
            Expr::Path(_) => "path",
            Expr::Range(_) => "range",
            Expr::Reference(_) => "reference",
            Expr::Repeat(_) => "repeat",
            Expr::Return(_) => "return",
            Expr::Struct(_) => "struct",
            Expr::Try(_) => "try",
            Expr::TryBlock(_) => "try_block",
            Expr::Tuple(_) => "tuple",
            Expr::Unary(_) => "unary",
            Expr::Unsafe(_) => "unsafe",
            Expr::Verbatim(_) => "verbatim",
            Expr::While(_) => "while",
            Expr::Yield(_) => "yield",
            _ => "unknown",
        })
        .with_span(expr)
    }

    pub fn unexpected_lit_type(lit: &Lit) -> Self {
        Error::unexpected_type(match *lit {
            Lit::Str(_) => "string",
            Lit::ByteStr(_) => "byte string",
            Lit::Byte(_) => "byte",
            Lit::Char(_) => "char",
            Lit::Int(_) => "int",
            Lit::Float(_) => "float",
            Lit::Bool(_) => "bool",
            Lit::Verbatim(_) => "verbatim",
            _ => "unknown",
        })
        .with_span(lit)
    }

    pub fn unknown_value(value: &str) -> Self {
        Error::new(ErrorKind::UnknownValue(value.into()))
    }

    pub fn too_few_items(min: usize) -> Self {
        Error::new(ErrorKind::TooFewItems(min))
    }

    pub fn too_many_items(max: usize) -> Self {
        Error::new(ErrorKind::TooManyItems(max))
    }

    pub fn unknown_lit_str_value(value: &LitStr) -> Self {
        Error::unknown_value(&value.value()).with_span(value)
    }
}

impl Error {
    pub fn write_error(self) -> TokenStream {
        syn::Error::from(self).into_compile_error()
    }
}

impl Error {
    pub fn has_span(&self) -> bool {
        self.span.is_some()
    }

    pub fn with_span<T: Spanned>(mut self, node: &T) -> Self {
        if !self.has_span() {
            self.span = Some(node.span());
        }
        self
    }

    pub fn span(&self) -> Span {
        self.span.unwrap_or_else(Span::call_site)
    }

    pub fn explicit_span(&self) -> Option<Span> {
        self.span
    }

    pub fn at<T: Display>(mut self, location: T) -> Self {
        self.locations.insert(0, location.to_string());
        self
    }

    pub fn at_path(self, path: &Path) -> Self {
        self.at(path_to_string(path))
    }

    pub fn prepend_at(mut self, mut locations: Vec<String>) -> Self {
        if !locations.is_empty() {
            locations.extend(self.locations);
            self.locations = locations;
        }
        self
    }

    pub fn location(&self) -> Vec<&str> {
        self.locations.iter().map(|i| i.as_str()).collect()
    }
}

impl StdError for Error {
    fn description(&self) -> &str {
        self.kind.description()
    }

    fn cause(&self) -> Option<&dyn StdError> {
        None
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.kind)?;
        if !self.locations.is_empty() {
            write!(f, " at {}", self.locations.join("/"))?;
        }
        Ok(())
    }
}

impl From<syn::Error> for Error {
    fn from(e: syn::Error) -> Self {
        Self {
            span: Some(e.span()),
            ..Self::custom(e)
        }
    }
}

impl From<Error> for syn::Error {
    fn from(e: Error) -> Self {
        if let Some(span) = e.explicit_span() {
            syn::Error::new(span, e.kind)
        } else {
            syn::Error::new(e.span(), e)
        }
    }
}
